package com.jh.user.service;

import cn.jh.common.core.base.MoreStrings;
import cn.jh.common.utils.ResultWrap;
import cn.jh.common.utils.TokenUtil;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.ImmutableMap;
import com.jh.user.business.PaymentProfitBusiness;
import com.jh.user.pojo.payment.PaymentProfit;
import com.jh.user.pojo.payment.PosDTO;
import com.jh.user.util.CommonsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.orm.jpa.JpaOptimisticLockingFailureException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.AsyncRestOperations;
import org.springframework.web.client.RestOperations;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


/**
 * @author Fuchun
 * @since 1.0
 */
@RestController
@RequestMapping("/v1.0/user")
public class PaymentProfitService {

    @SuppressWarnings("unchecked")
    private static final Class<Map<String, Object>> MAP_CLASS =
            (Class<Map<String, Object>>) TypeFactory.defaultInstance()
                    .constructMapType(Map.class, String.class, Object.class).getRawClass();
    private static final String VERIFY_ORDER_STATS_URL =
            "http://paymentchannel/v1.0/paymentchannel/order_stats/verify";

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final PaymentProfitBusiness paymentProfitBusiness;
    private final ScheduledExecutorService executorService;
    private final RestOperations restOperations;

    @Autowired
    public PaymentProfitService(
            PaymentProfitBusiness paymentProfitBusiness,
            ScheduledExecutorService executorService,
            @Qualifier("restOperations") RestOperations restOperations) {
        this.paymentProfitBusiness = paymentProfitBusiness;
        this.executorService = executorService;
        this.restOperations = restOperations;
    }

    /** 每天上午 9:00，下午 13:00 执行一次将分润表中的分润加至用户帐户。 */
    //@Scheduled(cron = "0 0 9,13 * * ?", zone = "Asia/Shanghai")
//    @Scheduled(cron = "0 0/30 * * * ?", zone = "Asia/Shanghai")
    public void syncProfitsToAccounts() {
        syncProfitsToAccounts(true);
    }

    /** 每天上午 8:10，下午 12:10 执行一次验证同步的数据。 */
    //@Scheduled(cron = "0 10 8,12 * * ?", zone = "Asia/Shanghai")
//    @Scheduled(cron = "0 0/30 * * * ?", zone = "Asia/Shanghai")
    @SuppressWarnings("unchecked")
    public void verifyPaymentProfits() {
        Pageable pageable = new PageRequest(0, 100, Sort.Direction.ASC, "createTime");
        Page<Long> posIdsPage;
        do {
            posIdsPage = paymentProfitBusiness.getInitialPaymentProfits(pageable);
            if (!posIdsPage.hasContent()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("No initial PaymentProfits, to finish verifyPaymentProfits().");
                }
                break;
            }

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
            HttpEntity<List<Long>> requestEntity = new HttpEntity<>(posIdsPage.getContent(), headers);

            ResponseEntity<Map<String, Object>> response = null;
            try {
                response = restOperations.postForEntity(
                        VERIFY_ORDER_STATS_URL, requestEntity, MAP_CLASS);
            } catch (Exception ex) {
                logger.error("POST {} failure: {}", VERIFY_ORDER_STATS_URL, ex.getMessage());
            }

            if (response != null) {
                HttpStatus statusCode = response.getStatusCode();
                if (!statusCode.is2xxSuccessful()) {
                    logger.error("POST {} Response: {} {}", VERIFY_ORDER_STATS_URL,
                            statusCode.value(), response.getBody());
                } else {
                    Map<String, Object> respMap = response.getBody();
                    Object resObj = respMap.get("result");
                    Map<String, Boolean> result = null;
                    if (resObj instanceof Map) {
                        result = (Map<String, Boolean>) resObj;
                    }

                    if (result != null && !result.isEmpty()) {
                        Map<Long, Boolean> posIdsToVerifiedMap = new HashMap<>(result.size());
                        for (Map.Entry<String, Boolean> entry : result.entrySet()) {
                            Long id = MoreStrings.toLong(entry.getKey(), null);
                            Boolean verified = entry.getValue();
                            if (id == null || verified == null) {
                                continue;
                            }
                            posIdsToVerifiedMap.put(id, verified);
                        }
                        paymentProfitBusiness.confirmPaymentProfits(posIdsToVerifiedMap);
                    }
                }
            }



            pageable = pageable.next();
        } while (posIdsPage.hasNext());
    }

    private void syncProfitsToAccounts(boolean scheduleIfFail) {
        Pageable pageable = new PageRequest(0, 50, Sort.Direction.ASC, "createTime");
        long total = 0;
        boolean optimisticLocking;
        int olRetryTimes = 0;
        Page<Long> userIdPage = null;
        do {
            if (olRetryTimes > 5) { // 乐观锁重试次数不能超过5次
                logger.error("重试执行 syncProfitsToAccounts() `{}` 次均失败（乐观锁异常）.", olRetryTimes);
                if (scheduleIfFail) {
                    logger.info("Schedule: 等待 10 分钟后，将自动执行第二次，直到成功为止。");
                    executorService.schedule(() -> syncProfitsToAccounts(false), 10, TimeUnit.MINUTES);
                }
                break;
            }
            try {
                // 如果正在执行转帐时用户帐户已被更改，则全部回滚并抛出乐观锁异常
                userIdPage = paymentProfitBusiness.syncProfitToAccount(pageable);

                total += userIdPage.getNumberOfElements();
                optimisticLocking = false;
                olRetryTimes = 0;
            } catch (JpaOptimisticLockingFailureException ex) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException ignored) {}
                optimisticLocking = true;
                olRetryTimes++;
                continue;
            }

            if (!userIdPage.hasNext()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("syncProfitsToAccounts completed. rows: {}", total);
                }
                break;
            }
            pageable = pageable.next();
        } while (optimisticLocking || userIdPage.hasNext());

        if (logger.isInfoEnabled()) {
            logger.info("执行分润打款计划任务完成，总共执行更新 {} 条记录。", total);
        }
    }

    /**
     * 手动执行分润转帐任务（补单）。
     *
     * POST /v1.0/user/payment_profits/retry_to_accounts
     *
     * {
     *     "resp_code": "000000",
     *     "resp_message": "OK"
     * }
     */
    @PostMapping(value = "/payment_profits/retry_to_accounts")
    public Map<String, Object> retryProfitsToAccounts() {
        syncProfitsToAccounts(false);
        return ResultWrap.ok();
    }

    /**
     * payment-channel 向 user 推送 PaymentProfit 数据。
     *
     * POST /v1.0/user/payment_profits/sync
     */
    @PostMapping(value = "/payment_profits/sync", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<Map<String, Object>> syncPaymentProfits(@RequestBody List<PosDTO> posList) {
        if (posList == null || posList.isEmpty()) {
            return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null);
        }
        Long syncCount = 0L;
        int retry = 2;
        while (retry > 0) {
            try {
                syncCount = paymentProfitBusiness.addPaymentProfits(posList);
                break;
            } catch (Exception ex) {
                logger.error("addPaymentProfits(...) exception: {}", ex.getMessage());
                // 自动重试1次保存数据
                try {
                    Thread.sleep(50);
                } catch (InterruptedException ignored) {}
            }
            retry--;
        }

        return ResponseEntity.ok().body(ImmutableMap.of(
                "syncCount", syncCount
        ));
    }
}
